Skip to content

feat: add the growthsignals activity taxonomy and emitter - #6057

Open
simplesagar wants to merge 5 commits into
mainfrom
grw-66-pr1-growthsignals-core
Open

feat: add the growthsignals activity taxonomy and emitter#6057
simplesagar wants to merge 5 commits into
mainfrom
grw-66-pr1-growthsignals-core

Conversation

@simplesagar

@simplesagar simplesagar commented Sep 4, 2026

Copy link
Copy Markdown
Member

Note

Stacked PR — merge bottom-up. Each PR targets the one above it, so its Files tab shows only its own diff.

  1. feat: add the growthsignals activity taxonomy and emitter #6057 — core taxonomy and emitter 👈 this PR
  2. feat: forward audited mutations to PostHog #6058 — audit stream to PostHog
  3. feat: report signups as invited or organic #6059 — direct emits: signup source, org created, member joined
  4. feat: report devices appearing in a fleet #6060 — devices
  5. feat: report agents new to an organization #6061 — agents

Merging #6057 retargets #6058 to main automatically, and so on down the stack.

GRW-66

First of a five-PR stack. Nothing calls this package yet, so no events are emitted and no behaviour changes.

Summary

Adds server/internal/growthsignals/, which describes notable moments in Gram as a single PostHog gram_activity event with a stable property shape:

  • The activity taxonomy, and the map from audit action to activity.
  • The event builder, which produces the event name, the distinct id, and the property map.
  • A repo-backed enricher behind a TTL cache, resolving organization, project and actor ids into the names a Slack reader needs.
  • The emitter, which skips the demo organization and logs capture failures rather than returning them.

Also includes the design document the rest of the stack follows.

Two decisions worth a reviewer's attention:

  • Uncurated audit actions pass through under a normalized name rather than being dropped, so the firehose channel has coverage automatically and there is no allowlist to keep extending. A small exclusion list keeps genuinely high-volume noise out.
  • dashboard_url is never omitted. A Slack destination that renders it as a button link fails the whole message when the url is empty, so it falls back to the organization's page and then to the site root.

Motivation

The events Growth wants to see do not exist in PostHog. There is no event for a project being created, an MCP server deployed, a security policy written, or a member joining an existing organization.

Every one of those mutations is already recorded by the audit logger and published to Pub/Sub, and gram streams already consumes that stream. The signal exists; nothing forwards it. This package is the shared vocabulary the rest of the stack uses to do that.

Temporal actions/month: 0, scales with fixed. No background work is added.


Summary by cubic

Adds the growthsignals package, which describes notable moments in Gram as a single PostHog gram_activity event with a stable property shape. This is PR 1 of a five-PR stack for GRW-66 (revamping PostHog Slack notifications); nothing calls the package yet, so no events are emitted and no behavior changes.

The package carries the activity taxonomy, the map from audit action to activity, the event builder, a repo-backed enricher behind a TTL cache, and the emitter, which skips the demo organization and logs capture failures rather than returning them. The design document the rest of the stack follows is included.

Reviewer notes

  • Uncurated audit actions pass through under a normalized name rather than being dropped, so the firehose has automatic coverage with no allowlist to maintain; a small exclusion list keeps genuinely high-volume noise out.
  • dashboard_url is never omitted: a Slack destination that renders it as a button link fails the whole message when the URL is empty, so it falls back to the organization's page and then to the site root. With no site URL configured, it is omitted rather than sent blank, so the template can drop the button instead of failing the message.
  • Per-activity extras cannot occupy reserved property keys, so an activity can never rewrite the event's identity.
  • Concurrent cache misses for the same key collapse into a single lookup that re-checks the cache before loading. The shared load runs detached from the caller that started it, so one caller's cancellation leaves without failing the query for everyone else waiting; a burst of events from one organization stays at one query.

Written for commit 2a19984. Summary will update on new commits.

Review in cubic

simplesagar and others added 2 commits September 3, 2026 20:31
Replaces the eleven ad-hoc Gram Slack destinations with a single
gram_activity event derived from the existing audit-log outbox stream,
plus purpose-built destinations for the significant, firehose and
all-events channels.

Temporal actions/month: 0 (outbox -> existing streams handler, no Temporal).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01761VcQZ1sW4TFb16AoozTA
Describes notable moments in Gram as a single PostHog `gram_activity`
event with a stable property shape: the activity taxonomy, the map from
audit action to activity, the event builder, a repo-backed enricher
behind a TTL cache, and the emitter itself.

Uncurated audit actions fall through to a normalized pass-through name so
the firehose has coverage without an allowlist to maintain, and a small
exclusion list keeps high-volume noise out. The emitter skips the demo
organization and logs capture failures rather than returning them, so a
dropped analytics event can never fail the request that produced it.

`dashboard_url` is the one property that is never omitted, because a
Slack destination that renders it as a button link fails the whole
message when the url is empty. It falls back to the organization's page
and then to the site root.

Nothing calls this package yet, so no events are emitted and no
behaviour changes.

Temporal actions/month: 0 (no background work added).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01761VcQZ1sW4TFb16AoozTA
@simplesagar
simplesagar requested a review from a team as a code owner September 4, 2026 03:32
@linear-code

linear-code Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

GRW-66

@changeset-bot

changeset-bot Bot commented Sep 4, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 2a19984

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
server Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@simplesagar simplesagar added the enhancement New feature or request label Sep 4, 2026

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 14 files

Tip: instead of fixing issues one by one fix them all with cubic

Re-trigger cubic

Comment thread server/internal/growthsignals/cache.go Outdated
Comment thread server/internal/growthsignals/enricher.go
Comment thread server/internal/growthsignals/event.go
Comment thread server/internal/growthsignals/event.go
Comment thread server/internal/growthsignals/actionmap.go
Three findings from review.

An extra could occupy a key the event shape owns. Writing extras first
was not enough on its own: a base property whose value is empty is
omitted rather than written, and the project keys are skipped entirely on
an organization-scoped activity, so an extra keyed `organization_slug`
survived whenever the slug did not resolve. Reserved keys are now
filtered when extras are copied.

`dashboard_url` is now omitted rather than reported blank when no site
URL is configured. An empty string is worse than an absent property: a
Slack destination rendering it as a button link fails the whole message
on a blank url, while an absent property lets the template omit the
button. The emitter says so once at construction.

Concurrent misses for the same key now collapse into one load. A burst of
stream messages from one organization all miss together on the first
event, and each was issuing its own query for the same row.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01761VcQZ1sW4TFb16AoozTA

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 4 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread server/internal/growthsignals/cache.go Outdated
Comment thread server/internal/growthsignals/cache.go Outdated
Comment thread server/internal/growthsignals/event_test.go
Two findings on the coalescing added in the previous commit, plus a nit.

The flight now re-reads the cache before loading. A caller that missed
before an earlier flight finished could still enter the callback after
that flight left the group, issuing a second query for a row already
cached.

Waiting on somebody else's query no longer ignores this caller's context.
A canceled stream handler was held until an unrelated request's database
query returned; it now leaves on cancellation while the flight continues
for whoever else is waiting.

The reserved-key test uses the exported property constant rather than a
literal, matching its neighbour.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01761VcQZ1sW4TFb16AoozTA

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 2 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Fix all with cubic | Re-trigger cubic

Comment thread server/internal/growthsignals/cache.go
The coalesced flight inherited the context of whichever caller happened
to start it, so that caller going away failed the lookup for everyone
still waiting on it — the opposite of what coalescing is for.

The shared load now runs on a context detached from cancellation but
keeping its values, so it stays inside the originating trace, bounded by
its own timeout so a detached query cannot outlive its usefulness.
Callers still leave on their own cancellation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01761VcQZ1sW4TFb16AoozTA
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants